
module ChessData
  
  # Used to indicate an error in making a move.
  class InvalidMoveError < RuntimeError
  end

  # Moves is a collection of regular expressions and methods to recognise 
  # how moves are written in PGN files, and to make the moves on a given 
  # board.
  # 
  # As the moves are obtained from PGN files, they are assumed to be correct.
  #
  module Moves

    # :nodoc:
    # Regular expressions to match each of the move types.
    Square = /[a-h][1-8]/ 
    Piece = /[KQRBN]/
    MatchKingsideCastles = /^O-O\+?\Z/
    MatchQueensideCastles = /^O-O-O\+?\Z/
    MatchPieceMove = /^(#{Piece})([a-h]?|[1-8]?)x?(#{Square})\+?\Z/
    MatchPawnCapture = /^([a-h])x(#{Square})\+?\Z/
    MatchPromotionPawnMove = /^([a-h][18])=([QqRrBbNn])\+?\Z/
    MatchSimplePawnMove = /^(#{Square})\+?\Z/
    MatchPromotionPawnCapture = /^([a-h])x([a-h][18])=([QqRrBbNn])\+?\Z/

    # Combined regular expression, to match a legal move.
    LegalMove = /#{MatchKingsideCastles}|#{MatchQueensideCastles}|#{MatchPieceMove}|#{MatchPawnCapture}|#{MatchPromotionPawnMove}|#{MatchSimplePawnMove}|#{MatchPromotionPawnCapture}/
    # :doc:

    # Returns an instance of the appropriate move type.
    # string:: a move read from a PGN file.
    def Moves.new_move string
      case string
      when MatchKingsideCastles then KingsideCastles.new
      when MatchQueensideCastles then QueensideCastles.new
      when MatchPieceMove then PieceMove.new string
      when MatchPawnCapture then PawnCapture.new string
      when MatchPromotionPawnMove then PromotionPawnMove.new string
      when MatchSimplePawnMove then SimplePawnMove.new string
      when MatchPromotionPawnCapture then PromotionPawnCapture.new string
      else raise InvalidMoveError.new("Invalid move: #{string}")
      end
    end

    # Return true if given _piece_ can move from _start_ square to _finish_ on given _board_.
    def Moves.can_reach board, piece, start, finish
      start = start.upcase
      finish = finish.upcase
      case piece
      when "K", "k" then Moves.king_can_reach start, finish
      when "Q", "q" then Moves.queen_can_reach board, start, finish
      when "R", "r" then Moves.rook_can_reach board, start, finish
      when "B", "b" then Moves.bishop_can_reach board, start,finish
      when "N", "n" then Moves.knight_can_reach start, finish
      end
    end

    private

    # Return true if moving the giving piece from start to finish
    # will leave the moving side's king in check.
    def Moves.king_left_in_check board, piece, start, finish
      test_board = board.clone
      test_board[start] = nil
      test_board[finish] = piece

      if board.to_move == "w"
        test_board.white_king_in_check?
      else
        test_board.black_king_in_check?
      end
    end

    def Moves.king_can_reach start, finish
      Moves.step_h(start, finish) <= 1 && Moves.step_v(start, finish) <= 1
    end

    def Moves.queen_can_reach board, start,finish
      Moves.rook_can_reach(board, start, finish) ||
        Moves.bishop_can_reach(board, start, finish)
    end

    def Moves.rook_can_reach board, start, finish
      start_col, start_row = Board.square_to_coords start
      end_col, end_row = Board.square_to_coords finish
      if start_col == end_col # moving along column
        row_1 = [start_row, end_row].min + 1
        row_2 = [start_row, end_row].max - 1
        row_1.upto(row_2) do |row|
          return false unless board[Board.coords_to_square(start_col, row)] == nil
        end
      elsif start_row == end_row # moving along row
        col_1 = [start_col, end_col].min + 1
        col_2 = [start_col, end_col].max - 1
        col_1.upto(col_2) do |col|
          return false unless board[Board.coords_to_square(col, start_row)] == nil
        end
      else
        return false
      end
      return true
    end

    def Moves.bishop_can_reach board, start,finish
      return false unless Moves.step_h(start,finish) == Moves.step_v(start, finish)
      start_col, start_row = Board.square_to_coords start
      end_col, end_row = Board.square_to_coords finish
      dirn_h = (end_row - start_row) / (end_row - start_row).abs
      dirn_v = (end_col - start_col) / (end_col - start_col).abs
      1.upto(Moves.step_h(start,finish)-1) do |i|
        square = Board.coords_to_square(start_col+(i*dirn_v), 
                                        start_row+(i*dirn_h))
        unless board[square] == nil
          return false 
        end
      end
      return true
    end

    def Moves.knight_can_reach start, finish
      h = Moves.step_h start, finish
      v = Moves.step_v start, finish
      return (h == 2 && v == 1) || (h == 1 && v == 2)
    end

    # Return size of horizontal gap between start and finish
    def Moves.step_h start, finish
      (start.bytes[0] - finish.bytes[0]).abs
    end

    # Return size of vertical gap between start and finish
    def Moves.step_v start, finish
      (start.bytes[1] - finish.bytes[1]).abs
    end

    # Methods to support king-side castling move.
    class KingsideCastles
      def to_s
        "O-O"
      end

      # Depending on the colour to move, will either castle king-side for white or black.
      # Returns a new instance of the board.
      def make_move board
        if board.to_move == "w"
          white_castles board
        else
          black_castles board
        end
      end
      
      private
      def white_castles board
        raise InvalidMoveError.new("white O-O") unless board["E1"] == "K" && 
          board["F1"] == nil && board["G1"] == nil && 
          board["H1"] == "R" && board.white_king_side_castling 

        revised_board = board.clone
        revised_board["E1"] = nil
        revised_board["F1"] = "R"
        revised_board["G1"] = "K"
        revised_board["H1"] = nil

        revised_board.to_move = "b"
        revised_board.enpassant_target = "-"
        revised_board.halfmove_clock += 1
        revised_board.white_king_side_castling = false
        revised_board.white_queen_side_castling = false

        return revised_board
      end

      def black_castles board
        raise InvalidMoveError.new("black O-O") unless board["E8"] == "k" && 
          board["F8"] == nil && board["G8"] == nil && 
          board["H8"] == "r" && board.black_king_side_castling 

        revised_board = board.clone
        revised_board["E8"] = nil
        revised_board["F8"] = "r"
        revised_board["G8"] = "k"
        revised_board["H8"] = nil

        revised_board.to_move = "w"
        revised_board.enpassant_target = "-"
        revised_board.halfmove_clock += 1
        revised_board.fullmove_number += 1
        revised_board.black_king_side_castling = false
        revised_board.black_queen_side_castling = false

        return revised_board
      end
    end

    # Methods to support queen-side castling move.
    class QueensideCastles
      def to_s
        "O-O-O"
      end

      # Depending on the colour to move, will either castle queen-side for white or black.
      # Returns a new instance of the board.
      def make_move board
        if board.to_move == "w"
          white_castles board
        else
          black_castles board
        end
      end

      private
      def white_castles board
        raise InvalidMoveError.new("white O-O-O") unless board["E1"] == "K" && 
          board["D1"] == nil && board["C1"] == nil && 
          board["B1"] == nil && board["A1"] == "R" && 
          board.white_queen_side_castling 

        revised_board = board.clone
        revised_board["E1"] = nil
        revised_board["D1"] = "R"
        revised_board["C1"] = "K"
        revised_board["B1"] = nil
        revised_board["A1"] = nil

        revised_board.to_move = "b"
        revised_board.enpassant_target = "-"
        revised_board.halfmove_clock += 1
        revised_board.white_king_side_castling = false
        revised_board.white_queen_side_castling = false

        return revised_board
      end

      def black_castles board
        raise InvalidMoveError.new("black O-O") unless board["E8"] == "k" && 
          board["D8"] == nil && board["C8"] == nil && 
          board["B8"] == nil && board["A8"] == "r" && 
          board.black_queen_side_castling 

        revised_board = board.clone
        revised_board["E8"] = nil
        revised_board["D8"] = "r"
        revised_board["C8"] = "k"
        revised_board["B8"] = nil
        revised_board["A8"] = nil

        revised_board.to_move = "w"
        revised_board.enpassant_target = "-"
        revised_board.halfmove_clock += 1
        revised_board.fullmove_number += 1
        revised_board.black_king_side_castling = false
        revised_board.black_queen_side_castling = false

        return revised_board
      end
    end

    # Methods to support a simple pawn move, moving directly forward.
    class SimplePawnMove
      def initialize move
        @move_string = move
        move =~ MatchSimplePawnMove
        @destination = $1
      end

      def to_s
        @move_string
      end

      # Returns a new instance of the board after move is made.
      def make_move board
        if board.to_move == "w"
          white_move board
        else
          black_move board
        end
      end

      private 
      def white_move board
        revised_board = board.clone

        if single_step board
          revised_board[@destination] = "P"
          revised_board[previous_square(board.to_move)] = nil
          revised_board.enpassant_target = "-"
        elsif initial_step board
          revised_board[@destination] = "P"
          revised_board[initial_square(board.to_move)] = nil
          revised_board.enpassant_target = previous_square(board.to_move)
        else
          raise InvalidMoveError.new "white #{@move_string}"
        end

        revised_board.to_move = "b"
        revised_board.halfmove_clock = 0

        return revised_board
      end

      def black_move board
        revised_board = board.clone

        if single_step board
          revised_board[@destination] = "p"
          revised_board[previous_square(board.to_move)] = nil
          revised_board.enpassant_target = "-"
        elsif initial_step board
          revised_board[@destination] = "p"
          revised_board[initial_square(board.to_move)] = nil
          revised_board.enpassant_target = previous_square(board.to_move)
        else
          raise InvalidMoveError.new "black #{@move_string}"
        end

        revised_board.to_move = "w"
        revised_board.halfmove_clock = 0
        revised_board.fullmove_number += 1

        return revised_board
      end

      def single_step board
        if board.to_move == "w"
          pawn = "P"
        else 
          pawn = "p"
        end
        board[@destination] == nil &&
          board[previous_square(board.to_move)] == pawn
      end

      def initial_step board
        if board.to_move == "w"
          pawn = "P"
          rank = 4
        else 
          pawn = "p"
          rank = 5
        end
        board[@destination] == nil && @destination[1].to_i == rank && 
          board[initial_square(board.to_move)] == pawn
      end

      def previous_square colour
        if colour == "w"
          offset = -1
        else
          offset = +1
        end
        "#{@destination[0]}#{@destination[1].to_i+offset}"
      end

      def initial_square colour
        if colour == "w"
          initial_rank = 2
        else
          initial_rank = 7
        end
        "#{@destination[0]}#{initial_rank}"
      end
    end

    # Methods to support a pawn move leading to promotion.
    class PromotionPawnMove < SimplePawnMove
      def initialize string
        @move_string = string
        string =~ MatchPromotionPawnMove
        string.split("=")
        @destination = $1
        @piece = $2
      end

      # Returns a new instance of the board after move is made.
      def make_move board
        @piece.downcase! if board.to_move == "b"
        revised_board = super board
        revised_board[@destination] = @piece
        return revised_board
      end
    end

    # Methods to support a pawn move which makes a capture.
    class PawnCapture
      def initialize move
        @move_string = move
        move =~ MatchPawnCapture
        @source = $1
        @destination = $2
      end

      def to_s
        @move_string
      end

      # Returns a new instance of the board after move is made.
      def make_move board
        origin = find_origin board.to_move

        revised_board = board.clone
        if @destination == board.enpassant_target
          revised_board["#{@destination[0]}#{origin[1]}"] = nil
        end
        revised_board[origin] = nil
        revised_board[@destination] = board[origin]
        revised_board.enpassant_target = "-"
        revised_board.halfmove_clock = 0
        if board.to_move == "w"
          revised_board.to_move = "b"
        else
          revised_board.to_move = "w"
          revised_board.fullmove_number += 1
        end

        return revised_board
      end

      private
      # For a pawn capture, find the originating row and create origin square
      def find_origin to_move
        row = @destination[1].to_i
        if to_move == "w"
          row -= 1
        else
          row += 1
        end

        return "#{@source}#{row}"
      end
    end

    # Methods to support a pawn move which is both a capture and a promotion.
    class PromotionPawnCapture < PawnCapture
      def initialize move
        @move_string = move
        move =~ MatchPromotionPawnCapture
        @source = $1
        @destination = $2
        @piece = $3
      end

      def to_s
        @move_string
      end

      # Returns a new instance of the board after move is made.
      def make_move board
        @piece.downcase! if board.to_move == "b"
        revised_board = super board
        revised_board[@destination] = @piece
        return revised_board
      end
    end

    # Methods to support a piece move.
    class PieceMove
      def initialize move
        @move_string = move
        move =~ MatchPieceMove
        @piece = $1
        @identifier = $2
        @destination = $3
        @is_capture = move.include? "x"
      end

      def to_s
        @move_string
      end

      # Returns a new instance of the board after move is made.
      def make_move board
        @piece.downcase! if board.to_move == "b"
        # for given piece type, locate those pieces on board which can reach destination
        origin = board.locations_of(@piece, @identifier).select do |loc| 
          Moves.can_reach(board, @piece, loc, @destination)
        end
        # filter out ambiguities raised by king being left in check
        if origin.length > 1
          origin = origin.delete_if do |loc|
            Moves.king_left_in_check(board, @piece, loc, @destination)
          end
        end
        # there should only be one unique piece at this point
        # raise an InvalidMoveError if not
        unless origin.length == 1 && board[origin.first] == @piece
          raise InvalidMoveError, "Not a unique/valid choice for #{@piece} to #{@destination}"
        end
        # setup a revised board with the move completed
        revised_board = board.clone
        revised_board[origin.first] = nil
        revised_board[@destination] = @piece
        revised_board.to_move = case board.to_move
                                when "w" then "b"
                                when "b" then "w"
                                end
        revised_board.enpassant_target = "-"
        if @is_capture
          revised_board.halfmove_clock = 0
        else
          revised_board.halfmove_clock += 1
        end
        revised_board.fullmove_number += 1 if board.to_move == "b"

        return revised_board
      end
    end
  end

end

